---
title: "Use the CSSStyleSheets API in a React App"
description: "How to use the new CSSStyleSheet JS API in your create-react-app application, for dynamic loading and unloading of CSS in JS"
slug: css-style-sheets
date: 2023-02-22T07:00:00.000Z
keywords: [react, cssstylesheets,css]
tags: [web]
authors: [forrest]
---

<!--
  ~ Copyright by LunaSec (owned by Refinery Labs, Inc)
  ~
  ~ Licensed under the Creative Commons Attribution-ShareAlike 4.0 International
  ~ (the "License"); you may not use this file except in compliance with the
  ~ License. You may obtain a copy of the License at
  ~
  ~ https://creativecommons.org/licenses/by-sa/4.0/legalcode
  ~
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  ~
-->
There's a shiny new web feature in browser town, and it's called [CSSStyleSheets](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet).

### What is CSSStyleSheets?

CSSStyleSheets allows you to manipulate page styling without having to load CSS anywhere in the HTML of the page.

With CSSStyleSheets you can do things like:

```js
const sheet = new CSSStyleSheet();
// Apply a rule to the sheet
sheet.replaceSync("a { color: red; }");
// disable the sheet to remove it from the DOM
sheet.disabled = true;
```

Neat. Support just became widespread, with adoption by Chrome, Safari, Firefox, etc [added in the last](https://caniuse.com/mdn-api_cssstylesheet) year (as of early 2023).

<!--truncate-->

### The Old Way
Whether you noticed it or not, your current css framework is almost certainly creating a
`<link href="example.css" rel="stylesheet">` in the HTML of the page or setting style attributes directly on page elements. Up until recently that was the only way to do it.

This works fine but it has a few disadvantages in certain cases. It doesn't play as nicely with dynamic usage, whether that's developer experience as you change files
or just switching sheets on and off on the fly.

Traditional CSS loading isn't going away any time soon, but the new CSSStyleSheet is great to have in your toolbelt.

### Why we wanted to use CSSStyleSheets

Our app, [LunaTrace](https://lunatrace.lunasec.io), has both a dark mode and a light mode. We compile two different stylesheets,
a `dark.css` and a `light.css`.

Previously we simply had a `<link href="dark.css"...` as above and, when we wanted to change to a light theme,
we reached into the DOM to modify the element to reference `light.css`. This worked ok but it was a little bit slow (the page flashed, not the end of the world), but it was a painfully slow developer experience. We had a separate watcher script from our main `react-scripts` that watched our SCSS files and recompiled them, and then the page would reload after
10 or 15 seconds. That was just a little too slow for making fast changes to the CSS, and the full page reload was very painful for a developer spoiled by Hot Module Reloading.

:::note
There are probably other ways to switch out styles for a page between a light and a dark mode rather than using the CssStyleSheet API, like a traditional class based selector. This is just one option and it was a good way for us to learn about the new API.
:::

### What is CSS-in-JS?

CSS-in-JS is when you put JS in charge of loading styles into the DOM. It does have pros and cons (that we won't get into), but the developer experience is great and it's much faster.

We could do `require 'dark.scss'` at the top of one of our TSX files and Create React App's webpack config would take care of the rest.

The only problem is: there is no way to _unload_ a global style you've loaded. For LunaTrace, we need to switch between dark and light themes, so that's a problem.

CSS-in-JS was incapable of doing something the DOM has been able to do for decades: unload something.

#### Manipulating a stylesheet with document.styleSheets
If we want to say, turn a stylesheet off, one way to get a reference to it is by the `document.styleSheets` property. This function can find a stylesheet
and return a CssStyleSheet object which we can manipulate directly.

```ts
function getStyleSheet(unique_title: string): CssStyleSheet {
  for (const sheet of document.styleSheets) {
    if (sheet.title === unique_title) {
      return sheet;
    }
  }
}
```

I couldn't figure out how to set the title of a stylesheet I was inserting with webpack (from `style-loader`) or find another way to tell which stylesheet was which, so this wasn't enough for me.

In hindsight, it might be possible to use the CSSStyleSheet API to look for some special rule you know will be there, or make a dummy rule for that purpose. I didn't try that.

Anyway, maybe this is enough to accomplish your goal. If not, and you'd like to import a CssStyleSheet object directly via webpack, read on.

### Injecting a CSSStyleSheet directly into JS

The webpack plugin `css-loader` thankfully does support this new API. It will compile your CSS import
into JavaScript which wraps your CSS file with `new CssStyleSheet` so that you can directly import and use it.

A simple example looks like:

```typescript
import darkStyles from '../scss/main/dark.css-style-sheet.scss';
import lightStyles from '../scss/main/light.css-style-sheet.scss';

// now mount one of those to the dom
document.adoptedStyleSheets = [darkStyles]
```

Note that we add the stylesheet to the `adoptedStyleSheets` array on the document object (which is what gets it into the DOM). This is like `document.styleSheets` except that it
isn't immutable and can be modified directly by JavaScript, and that's exactly what we need.

If you have your own webpack config, just add a rule using `css-loader` and set `exportType: 'css-style-sheet'`, as per the [css-loader docs](https://webpack.js.org/loaders/css-loader/#exporttype).

Unfortunately, that won't work out of the box with Create React App (CRA). There are no loaders set up to compile to that format. That's
understandable since it's pretty new. Instead, let's use Craco, a hookable wrapper around CRA, to get the loader we need.

### Taking back control of our webpack config using Craco
:::info
If you're already using Craco or have your own webpack config you can skip ahead.
:::

`npm i --save-dev @craco/craco`

or

`yarn add -D @craco/craco`

Create a craco config file in your project root
 ```ts title="craco.config.js"
module.exports = {
  webpack: {
    configure: (webpackConfig, { env, paths }) => {
      // Modify the webpackConfig here

      return webpackConfig;
    },
  },
};
 ```

Replace your package.json CRA scripts with Craco ones:
```json title="package.json"
"scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test"
}
```

### Configure the new rule

I set up a new rule in the Craco config that only matches files that end in `.css-style-sheet.scss`

```js
configure: (webpackConfig, {env, paths}) => {

    webpackConfig.module.rules[1].oneOf.unshift(
        {
            test: /\.css-style-sheet\.(scss|sass)$/,
            use: [{
                loader: require.resolve("css-loader"),
                options: {
                    exportType: 'css-style-sheet'
                },
            }, {
                loader: require.resolve('postcss-loader'),
                options: {
                    postcssOptions: {
                        ident: 'postcss',
                        config: false,
                        plugins: [
                            'postcss-flexbugs-fixes',
                            [
                                'postcss-preset-env',
                                {autoprefixer: {flexbox: 'no-2009'}, stage: 3}
                            ],
                            'postcss-normalize'
                        ]
                    },
                    sourceMap: true
                }
            }, 'sass-loader']
        });
    return webpackConfig;
},
```

That's pretty verbose, but the key part is only the one line `exportType: 'css-style-sheet'`. The rest is just trying to
stay consistent with the rest of how CRA uses webpack. I'm not sure exactly what bugs are fixed by `postcss-loader`, but I'd rather not find out by skipping it.

If you don't want to use SCSS you could change the `test` at the top and take out the `sass-loader` at the bottom. Same thing. The key part is getting
the `css-loader` into the top of the rules, and getting it out from under `style-loader` that typically injects styles into the DOM, since it won't work with that in `css-style-sheet` mode.

### That's it

Done! That was a little more painful than I hoped for, but the end result speaks for itself. Modifying the webpack rules by index as I've done is perhaps a little bit brittle,
but I expect this to continue working for some time.

I [opened a discussion](https://github.com/facebook/create-react-app/discussions/13030) on the CRA github, and I'd happily PR this feature into CRA if I knew it would be accepted.

### Some complaints

#### Create React App can and should be way more expandable
By the way, having to use Craco at all (or worse, eject) is a huge downside to CRA. Vue-cli takes a _far_ more
[extendable approach](https://cli.vuejs.org/guide/webpack.html) to build configuration, including an excellent plugin system. Maybe someday
CRA will do something similar (but I stopped holding my breath a while ago).

#### This feature is _really_ poorly named

Nothing in the name "CssStyleSheet" is at all unique to this feature. It's a good name for the `class` in JavaScript; however, it makes it almost
impossible to search for since the name is so completely ambiguous with other previous CSS loading methods. That's no good. I'd much rather
this be called `ControlledCSS` or `DynamicCSS` or something similar.

Naming, after all, is most of programming. If I can't find something, I won't know to use it.

